package spring.cloud.tasks.common;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
 * @author huihui
 */
@Slf4j
@Setter
@Getter
public class SuspendableCronExpression {
    /**
     * cron表达式字符串
     */
    private String cronExpressionString;
    /**
     * has tested,max time to 2122-12-31 23:59:59
     */
    private org.quartz.CronExpression cronExpression = null;
    /**
     * 任务暂停时间段，日期段。
     */
    private String pauseDatePeriod = "";
    /**
     * 任务暂停时间段，小时分钟段。
     */
    private String pauseTimePeriod = "";
    /**
     * 如果任务CRON+暂停日期段+暂停时间段三则改变，那么签名需要改变。需要重新生成执行点列表。
     */
    private long version;//任务调度配置的版本信息。如果调度时间没有改，那么这个则不会改变。一般情况生产上调度时间规则变化的很少。

    //
    public SuspendableCronExpression(long fireDateTimeVersion, String cronExpressionString, String pauseDatePeriod, String pauseTimePeriod) {
        //
        this.cronExpressionString = cronExpressionString;
        try {
            this.cronExpression = new org.quartz.CronExpression(cronExpressionString);
        } catch (ParseException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e);
        }
        this.pauseDatePeriod = pauseDatePeriod;
        this.pauseTimePeriod = pauseTimePeriod;
        this.version = fireDateTimeVersion;
        computeFirstFireTime();//判断当前cron表达式是否会造成死循环问题
    }

    private Date computeFirstFireTime() {
        Date startTime = new Date();
        Date nextFireTime = getFireTimeAfter0(new Date(startTime.getTime() - 1000L));
        if (nextFireTime == null) {
            throw new NullPointerException("nextFireTime");
        }
        //
        int count = 0;
        while (nextFireTime != null && isInPausePeriod(nextFireTime)) {
            count++;
            if (count > 1024 * 10000) {
                throw new IllegalStateException("获取下次执行时间失败,请检查cron表达式和暂停日期段和暂停时间段是否正确设置");
            }
            nextFireTime = getFireTimeAfter0(nextFireTime);
        }
        return nextFireTime;
    }

    public Date getNextFireTimeAfter(Date afterTime) {
        if (afterTime == null) {
            throw new NullPointerException("afterTime");
        }
        Date nextFireTime = getFireTimeAfter0(afterTime);
        //
        while (nextFireTime != null && isInPausePeriod(nextFireTime)) {
            nextFireTime = getFireTimeAfter0(nextFireTime);
        }
        return nextFireTime;
    }

    /**
     * 最底层的方法
     */
    private Date getFireTimeAfter0(Date afterTime) {
        if (afterTime == null) {
            throw new NullPointerException("afterTime");
        }
        Date date = cronExpression.getNextInvalidTimeAfter(afterTime);
        if (date == null) {
            throw new NullPointerException("底层计算出的下次运行时间为空");
        }
        return date;
    }

    /**
     * 最底层的方法
     * 暂停日期段/暂停时间段:假定业务需要每天某几个时间段不允许task进行调度,则对暂停时间段进行配置即可。 如果需要对某些天不进行任务调度。那么只需要配置暂停日期段即可。暂停日期段会覆盖暂停时间段的配置。
     * <pre>
     *        判断逻辑也可以这样:但是没有下面的代码精简
     *         if (!pausePeriodDateIsNotEmpty&&!pausePeriodTimeIsNotEmpty){
     *             return false;//都没有配置则不在暂停区间
     *         }else if (pausePeriodDateIsNotEmpty&&!pausePeriodTimeIsNotEmpty){
     *             return dateIn;//只关心是否在date暂停区间
     *         }else if (!pausePeriodDateIsNotEmpty&&pausePeriodTimeIsNotEmpty){
     *                 return timeIn;//只关心是否在time暂停区间
     *         }else {
     *             //pausePeriodDateIsNotEmpty&&pausePeriodTimeIsNotEmpty 都配置了 只要其中一个区间满足则处于暂停区间
     *             return dateIn||timeIn;
     *         }
     * </pre>
     */
    private boolean isInPausePeriod(Date date) {
        Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
        calendar.setTime(date);
        int month = calendar.get(Calendar.MONTH) + 1; // Calendar.MONTH begin from 0.
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int minute = calendar.get(Calendar.MINUTE);
        //
        boolean pausePeriodDateIsNotEmpty = !(pauseDatePeriod == null || pauseDatePeriod.trim().isEmpty());//取反才是最精确的,允许空格存在
        if (pausePeriodDateIsNotEmpty) {
            try {
                boolean dateIn = checkIsInDateOrTimePausePeriod(pauseDatePeriod, "/", month, day);
                if (dateIn) {
                    return true;
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new RuntimeException("暂停日期段格式错误");
            }
        }
        //
        boolean pausePeriodTimeIsNotEmpty = !(pauseTimePeriod == null || pauseTimePeriod.trim().isEmpty());//取反才是最精确的,允许空格存在
        if (pausePeriodTimeIsNotEmpty) {
            try {
                boolean timeIn = checkIsInDateOrTimePausePeriod(pauseTimePeriod, ":", hour, minute);
                if (timeIn) {
                    return true;
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new RuntimeException("暂停时间段格式错误");
            }
        }
        return false;
    }

    /**
     * 最底层的方法
     *
     * @param periodsString 暂停日期段格式:01/02-03/04,05/06-07/08
     *                      暂停时间段格式:00:00-11:59,12:00-22:59
     *                      注意如果设置暂停时间段为00:00-11:59,12:00-23:59即一整天。
     *                      在计算执行时间列表的时候会造成长时间得不到下一个有效的执行时间造成任务编辑页面长时间等待。
     */
    private static boolean checkIsInDateOrTimePausePeriod(String periodsString, String splitChar, int monthOrHour, int dayOrMinute) {//
        try {
            String[] periodStrings = periodsString.split(Symbols.COMMA);
            for (String periodString : periodStrings) {
                String[] strings = periodString.trim().split("-");
                if (!(strings != null && strings.length == 2)) {
                    throw new RuntimeException("暂停日期/时间段格式错误");
                } else {
                    String left = strings[0].trim();
                    String right = strings[1].trim();
                    String[] lefts = left.split(splitChar);// splitChar为:/ or :
                    String[] rights = right.split(splitChar);//  splitChar为:/ or :
                    if (!(lefts != null && lefts.length == 2 && rights != null && rights.length == 2)) {
                        throw new RuntimeException("暂停日期/时间段格式错误");
                    } else {
                        int left1 = Integer.parseInt(lefts[0]);
                        int left2 = Integer.parseInt(lefts[1]);
                        int right1 = Integer.parseInt(rights[0]);
                        int right2 = Integer.parseInt(rights[1]);
                        boolean result = //
                                (monthOrHour > left1 || monthOrHour == left1 && dayOrMinute >= left2) //
                                        &&//
                                        (monthOrHour < right1 || monthOrHour == right1 && dayOrMinute <= right2);//
                        if (result) {
                            return true;//位于暂停区间
                        }
                    }
                }
            }
            return false;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("暂停日期/时间段格式错误", e);
        }
    }


}

